#pragma once

#include <stx/core/posix.hpp>

inline namespace stx
{
void murmurHash3_x86_32(void const*         key,
                        std::size_t const   len,
                        std::uint32_t const seed,
                        void*               out);

void murmurHash3_x86_128(void const*         key,
                         std::size_t const   len,
                         std::uint32_t const seed,
                         void*               out);

void murmurHash3_x64_128(void const*         key,
                         std::size_t const   len,
                         std::uint32_t const seed,
                         void*               out);

[[nodiscard]] constexpr bool
isBase64Character(IsOctet auto const octet) noexcept
{
    auto const c = static_cast<char>(octet);
    return isAlphanumeric(c) || ('+' == c) || ('/' == c);
}

[[nodiscard]] constexpr bool
isBase64String(IsStringLike auto const& str) noexcept
{
    auto const sv = tosv(str);
    return ranges::count_if(sv,
                            EXPR('=' == _1) || BIND(isBase64Character, _1)) ==
           std::ssize(sv);
}

[[nodiscard]] constexpr std::size_t
getBase64EncodedSize(std::size_t const bin_data_size) noexcept
{
    return static_cast<std::size_t>((4 * bin_data_size / 3) + 3) & ~3uz;
}

[[nodiscard]] constexpr std::size_t
getBase64DecodedSize(std::string_view const sv_b64) noexcept
{
    if (sv_b64.size() < 2)
    {
        return 0;
    }

    auto const char_cnt    = sv_b64.size();
    auto const sv_tail     = tosv(&sv_b64[char_cnt - 2], 2);
    auto const padding_cnt = std::count(sv_tail.begin(), sv_tail.end(), '=');

    return Number(3 * (char_cnt / 4)) - padding_cnt;
}

std::size_t base64Decode(std::string_view const sv_b64,
                         void*                  obuf,
                         std::size_t const      obuf_size);

[[nodiscard]] Buffer base64Decode(std::string_view const sv_b64);

[[nodiscard]] std::string base64Encode(void const* const data,
                                       std::size_t const size);

[[nodiscard]] inline std::string base64Encode(IsMemArea auto const& mm)
{
    auto const [data, size] = getMemAreaInfo(mm);
    return base64Encode(data, size);
}

// Percent-encoding == URL Encoding (RFC3986)

// RFC3986 requires:

// [*] Only [a-zA-Z0-9.~_-] can be direct used and cannot be percent-encoded.

// [*] Characters other than [a-zA-Z0-9.~_-] must be first encoded in UTF-8 and
//     then be percent-encoded. Such as %2D, %ab, %EF, %41, etc.

// In spite of percent encoding is case-insensitive, some protocals still
// require it to be case-sensitive. Uppercase is more prevalent.
[[nodiscard]] std::string percentEncode(void const* const data,
                                        std::size_t const size,
                                        SbLowercase const sb_lowercase);

[[nodiscard]] inline std::string percentEncode(IsMemArea auto const& input,
                                               SbLowercase const sb_lowercase)
{
    auto const [p, size] = getMemAreaInfo(input);
    return percentEncode(p, size, sb_lowercase);
}

[[nodiscard]] inline std::string percentEncode(IsMemArea auto const& input)
{
    return percentEncode(input, SbLowercase(false));
}

[[nodiscard]] Buffer percentDecode(std::string_view const sv_input);

enum class MessageDigestType
{
    K_MD5    = 16,
    K_SHA1   = 20,
    K_SHA224 = 28,
    K_SHA256 = 32,
    K_SHA384 = 48,
    K_SHA512 = 64,
};

template<MessageDigestType C_MSG_DIGEST_TYPE_>
class BasicMessageDigest final
    : public ByteArray<static_cast<std::size_t>(C_MSG_DIGEST_TYPE_)>
{
    static const MessageDigestType type = C_MSG_DIGEST_TYPE_;

    using Base_ = ByteArray<static_cast<std::size_t>(C_MSG_DIGEST_TYPE_)>;

public:
    using Base_::Base_;
};

using Md5    = BasicMessageDigest<MessageDigestType::K_MD5>;
using Sha1   = BasicMessageDigest<MessageDigestType::K_SHA1>;
using Sha224 = BasicMessageDigest<MessageDigestType::K_SHA224>;
using Sha256 = BasicMessageDigest<MessageDigestType::K_SHA256>;
using Sha384 = BasicMessageDigest<MessageDigestType::K_SHA384>;
using Sha512 = BasicMessageDigest<MessageDigestType::K_SHA512>;

// ??? https://stackoverflow.com/q/69806220/508343
template<MessageDigestType C_MSG_DIGEST_TYPE_>
[[nodiscard]] inline BasicMessageDigest<C_MSG_DIGEST_TYPE_>
digest(void const* const data, std::size_t const size)
{
    ASSERT(data);
    ASSERT(size > 0);

    if constexpr (MessageDigestType::K_MD5 == C_MSG_DIGEST_TYPE_)
    {
        auto ret = Md5{};
        auto ctx = ::EVP_MD_CTX_new();
        auto len = static_cast<unsigned int>(ret.size());

        ::EVP_DigestInit_ex(ctx, ::EVP_md5(), nullptr);
        ::EVP_DigestUpdate(ctx, data, size);
        ::EVP_DigestFinal_ex(
            ctx, std::bit_cast<unsigned char*>(ret.data()), &len);
        ::EVP_MD_CTX_free(ctx);

        return ret;
    }
    else if constexpr (MessageDigestType::K_SHA1 == C_MSG_DIGEST_TYPE_)
    {
        auto ret = Sha1{};
        auto ctx = ::EVP_MD_CTX_new();
        auto len = static_cast<unsigned int>(ret.size());

        ::EVP_DigestInit_ex(ctx, ::EVP_sha1(), nullptr);
        ::EVP_DigestUpdate(ctx, data, size);
        ::EVP_DigestFinal_ex(
            ctx, std::bit_cast<unsigned char*>(ret.data()), &len);
        ::EVP_MD_CTX_free(ctx);

        return ret;
    }
    else if constexpr (MessageDigestType::K_SHA224 == C_MSG_DIGEST_TYPE_)
    {
        auto ret = Sha224{};
        auto ctx = ::EVP_MD_CTX_new();
        auto len = static_cast<unsigned int>(ret.size());

        ::EVP_DigestInit_ex(ctx, ::EVP_sha224(), nullptr);
        ::EVP_DigestUpdate(ctx, data, size);
        ::EVP_DigestFinal_ex(
            ctx, std::bit_cast<unsigned char*>(ret.data()), &len);
        ::EVP_MD_CTX_free(ctx);

        return ret;
    }
    else if constexpr (MessageDigestType::K_SHA256 == C_MSG_DIGEST_TYPE_)
    {
        auto ret = Sha256{};
        auto ctx = ::EVP_MD_CTX_new();
        auto len = static_cast<unsigned int>(ret.size());

        ::EVP_DigestInit_ex(ctx, ::EVP_sha256(), nullptr);
        ::EVP_DigestUpdate(ctx, data, size);
        ::EVP_DigestFinal_ex(
            ctx, std::bit_cast<unsigned char*>(ret.data()), &len);
        ::EVP_MD_CTX_free(ctx);

        return ret;
    }
    else if constexpr (MessageDigestType::K_SHA384 == C_MSG_DIGEST_TYPE_)
    {
        auto ret = Sha384{};
        auto ctx = ::EVP_MD_CTX_new();
        auto len = static_cast<unsigned int>(ret.size());

        ::EVP_DigestInit_ex(ctx, ::EVP_sha384(), nullptr);
        ::EVP_DigestUpdate(ctx, data, size);
        ::EVP_DigestFinal_ex(
            ctx, std::bit_cast<unsigned char*>(ret.data()), &len);
        ::EVP_MD_CTX_free(ctx);

        return ret;
    }
    else if constexpr (MessageDigestType::K_SHA512 == C_MSG_DIGEST_TYPE_)
    {
        auto ret = Sha512{};
        auto ctx = ::EVP_MD_CTX_new();
        auto len = static_cast<unsigned int>(ret.size());

        ::EVP_DigestInit_ex(ctx, ::EVP_sha512(), nullptr);
        ::EVP_DigestUpdate(ctx, data, size);
        ::EVP_DigestFinal_ex(
            ctx, std::bit_cast<unsigned char*>(ret.data()), &len);
        ::EVP_MD_CTX_free(ctx);

        return ret;
    }
    else
    {
        static_assert(AlwaysFalse<BasicMessageDigest<C_MSG_DIGEST_TYPE_>>);
    }
}

template<MessageDigestType C_MSG_DIGEST_TYPE_>
[[nodiscard]] inline BasicMessageDigest<C_MSG_DIGEST_TYPE_>
digest(MemView const mv)
{
    return digest<C_MSG_DIGEST_TYPE_>(mv.data(), mv.size());
}

template<MessageDigestType C_MSG_DIGEST_TYPE_>
BasicMessageDigest<C_MSG_DIGEST_TYPE_> hmac(MemView const mv_key,
                                            MemView const mv)
{
    auto ctx = ::EVP_MAC_CTX_new(::EVP_MAC_fetch(nullptr, "HMAC", nullptr));
    ASSERT(ctx);
    ON_SCOPE_EXIT(::EVP_MAC_CTX_free(ctx));

    ::OSSL_PARAM params[2]{};

    if constexpr (MessageDigestType::K_MD5 == C_MSG_DIGEST_TYPE_)
    {
        params[0] = ::OSSL_PARAM_construct_utf8_string(
            OSSL_MAC_PARAM_DIGEST, const_cast<char*>("MD5"), 0);
        params[1] = OSSL_PARAM_construct_end();
    }
    else if constexpr (MessageDigestType::K_SHA1 == C_MSG_DIGEST_TYPE_)
    {
        params[0] = ::OSSL_PARAM_construct_utf8_string(
            OSSL_MAC_PARAM_DIGEST, const_cast<char*>("SHA1"), 0);
        params[1] = OSSL_PARAM_construct_end();
    }
    else if constexpr (MessageDigestType::K_SHA224 == C_MSG_DIGEST_TYPE_)
    {
        params[0] = ::OSSL_PARAM_construct_utf8_string(
            OSSL_MAC_PARAM_DIGEST, const_cast<char*>("SHA224"), 0);
        params[1] = OSSL_PARAM_construct_end();
    }
    else if constexpr (MessageDigestType::K_SHA256 == C_MSG_DIGEST_TYPE_)
    {
        params[0] = ::OSSL_PARAM_construct_utf8_string(
            OSSL_MAC_PARAM_DIGEST, const_cast<char*>("SHA256"), 0);
        params[1] = OSSL_PARAM_construct_end();
    }
    else if constexpr (MessageDigestType::K_SHA384 == C_MSG_DIGEST_TYPE_)
    {
        params[0] = ::OSSL_PARAM_construct_utf8_string(
            OSSL_MAC_PARAM_DIGEST, const_cast<char*>("SHA384"), 0);
        params[1] = OSSL_PARAM_construct_end();
    }
    else if constexpr (MessageDigestType::K_SHA512 == C_MSG_DIGEST_TYPE_)
    {
        params[0] = ::OSSL_PARAM_construct_utf8_string(
            OSSL_MAC_PARAM_DIGEST, const_cast<char*>("SHA512"), 0);
        params[1] = OSSL_PARAM_construct_end();
    }
    else
    {
        static_assert(AlwaysFalse<BasicMessageDigest<C_MSG_DIGEST_TYPE_>>);
    }

    ::EVP_MAC_init(ctx,
                   std::bit_cast<unsigned char*>(mv_key.data()),
                   mv_key.size(),
                   params);
    ::EVP_MAC_update(
        ctx, std::bit_cast<unsigned char const*>(mv.data()), mv.size());

    auto ret = BasicMessageDigest<C_MSG_DIGEST_TYPE_>{};
    auto len = ret.size();
    ::EVP_MAC_final(ctx, std::bit_cast<unsigned char*>(ret.data()), &len, len);
    ASSERT(ret.size() == Number(len));

    return ret;
}

[[nodiscard]] Md5    md5(MemView const mv);
[[nodiscard]] Sha1   sha1(MemView const mv);
[[nodiscard]] Sha224 sha224(MemView const mv);
[[nodiscard]] Sha256 sha256(MemView const mv);
[[nodiscard]] Sha384 sha384(MemView const mv);
[[nodiscard]] Sha512 sha512(MemView const mv);

[[nodiscard]] Md5    hmacMd5(MemView const mv_key, MemView const mv);
[[nodiscard]] Sha1   hmacSha1(MemView const mv_key, MemView const mv);
[[nodiscard]] Sha224 hmacSha224(MemView const mv_key, MemView const mv);
[[nodiscard]] Sha256 hmacSha256(MemView const mv_key, MemView const mv);
[[nodiscard]] Sha384 hmacSha384(MemView const mv_key, MemView const mv);
[[nodiscard]] Sha512 hmacSha512(MemView const mv_key, MemView const mv);

enum class AesKeyType
{
    K_128_BITS = 16,
    K_192_BITS = 24,
    K_256_BITS = 32,
};

template<AesKeyType K_AES_KEY_TYPE_>
class AesKey final : public ByteArray<static_cast<std::size_t>(K_AES_KEY_TYPE_)>
{
    using Base_ = ByteArray<static_cast<std::size_t>(K_AES_KEY_TYPE_)>;

public:
    using Base_::Base_;
    using Base_::load;

    explicit AesKey(std::string_view const sv_hex_key)
    {
        this->load(sv_hex_key);
    }

    bool load(std::string_view const sv_hex_key)
    {
        if (sv_hex_key.size() != this->size() * 2)
        {
            LOG_ERROR("Invalid hex string size: "sv,
                      sv_hex_key.size(),
                      _I_,
                      K_AES_KEY_TYPE_);
            return false;
        }

        hex2bin(sv_hex_key, this->data(), this->size());
        return true;
    }
};

using AesKey128 = AesKey<AesKeyType::K_128_BITS>;
using AesKey192 = AesKey<AesKeyType::K_192_BITS>;
using AesKey256 = AesKey<AesKeyType::K_256_BITS>;

class AesGcmIv final : public ByteArray<12>
{
    using Base_ = ByteArray<12>;

public:
    using Base_::Base_;
    using Base_::load;

    explicit AesGcmIv(std::string_view const sv_hex_iv);
    void load(std::string_view const sv_hex_iv);
};

class AesGcmTag final : public ByteArray<16>
{
    using Base_ = ByteArray<16>;

public:
    using Base_::Base_;
    using Base_::load;

    explicit AesGcmTag(std::string_view const sv_hex_tag);
    void load(std::string_view const sv_hex_tag);
};

struct AesGcmMsg final
{
    Buffer      data;
    std::string aad;
    AesGcmIv    iv;
    AesGcmTag   tag;
};

[[nodiscard]] std::optional<AesGcmMsg> decrypt(AesKey128 const& key,
                                               AesGcmMsg const& ciphertext);

[[nodiscard]] std::optional<AesGcmMsg> decrypt(AesKey192 const& key,
                                               AesGcmMsg const& ciphertext);

[[nodiscard]] std::optional<AesGcmMsg> decrypt(AesKey256 const& key,
                                               AesGcmMsg const& ciphertext);

[[nodiscard]] std::optional<AesGcmMsg> encrypt(AesKey128 const&  key,
                                               void const* const plaintext,
                                               std::size_t const plaintext_size,
                                               std::string       aad);

[[nodiscard]] std::optional<AesGcmMsg> encrypt(AesKey192 const&  key,
                                               void const* const plaintext,
                                               std::size_t const plaintext_size,
                                               std::string       aad);

[[nodiscard]] std::optional<AesGcmMsg> encrypt(AesKey256 const&  key,
                                               void const* const plaintext,
                                               std::size_t const plaintext_size,
                                               std::string       aad);

template<AesKeyType C_AES_KEY_BITS_>
[[nodiscard]] inline std::optional<AesGcmMsg>
encrypt(AesKey<C_AES_KEY_BITS_> const& key,
        IsMemArea auto const&          mm,
        std::string                    aad)
{
    auto const [data, size] = getMemAreaInfo(mm);
    return encrypt(key, data, size, std::move(aad));
}
} // namespace stx